'''
 * CaRComp Execution Environment
 * 
 * Copyright (C) 2011 Taylor L. Riche
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
'''

import queue
import threading
import optparse
import time
from carcomp.framework import component, io_component, connector, abstraction
from carcomp.framework.file_io import file_reader
from carcomp.framework.net_io import net_reader

class Application(object):
    '''
    An Application is a series of component.Implementation instances connected by 
    Connector instances. Each Implementation is executed by a CompThread.
    
    An Application instance is the basic wrapper for Implementation instances
    executing on one "machine."
    
    '''

    def __init__(self):
        '''
        Constructor 
        
        Takes no arguments.
        
        Initializes empty dictionaries for the Implementation instances
        and the Connector instances.

        '''
        self._components = {}
        self._connections = {}
        self._threads = []
        self._node_name = ''
        self._io_comp_list = []
        
    def instrument(self, root=None, uid=None):
        '''
        Converts all connectors into instrumented connectors.
        '''
        '''self._connections = {conn.get_src_name():conn.instrument() for conn in self._connections.values()}
        for some reason pydev isn't recognizing the dict comprehension
        '''
        temp_dict = {}
        for conn in self._connections.values():
            temp_dict[conn.get_src_name()] = conn.instrument(root, uid)
        self._connections = temp_dict
        
    def create_component(self, comp, ports=None):
        '''Registers a new Implementation and adds the ports listed in the tuple list ports. 
        
        ports is a list of (portname, handler) string tuples. comp must
        be a new instance of a subclass of Implementation. Have to do it this way to
        define handler methods in the subclass.
        '''
        if(not isinstance(comp, component.Implementation)):
            raise Exception
        self.add_component(comp)
        if(ports != None):
            [comp.add_port(component.Port(portTuple[0], portTuple[1])) for portTuple in ports]
        return comp
            
        
    def add_component(self, comp):
        '''Adds a Implementation to the dictionary using the Implementation name as the key.'''
        self._components[comp.get_name()] = comp
        if isinstance(comp, io_component.IOComponent):
            self._io_comp_list.append(comp)
        
    def add_components(self, comps):
        '''Adds a list of Componnets to the dictionary. Syntactic sugar.'''
        for comp in comps:
            self.add_component(comp) 
        
    def add_connection(self, conn):
        '''Adds a Connector to the dictionary using the source name as the key.'''
        self._connections[conn.get_src_name()] = conn
        
    def add_connections(self, conns):
        '''Adds a list of Connectors to the dictionary. Syntactic sugar.'''
        for conn in conns:
            self.add_connection(conn) 
        
    def create_connection(self, start, end):
        '''Create and add a connection to the application.
        
        start and end are tuples of component/port names.
        Returns the new connection.
        '''
        start_op = self._components[start[0]]
        start_ep = connector.Endpoint(start_op, start_op.get_port(start[1]))
        end_op = self._components[end[0]]
        end_ep = connector.Endpoint(end_op, end_op.get_port(end[1]))
        ret_val = connector.Connector(start_ep, end_ep)
        self.add_connection(ret_val)
        return ret_val
        
    def add_thread(self, t):
        '''Adds a CompThread object to the set of threads in the Application.'''
        self._threads.append(t)
        
    def add_threads(self, threads):
        '''Adds a list of CompThread objects to the set of threads. Syntactic sugar.'''
        for t in threads:
            self.add_thread(t)
            
    def create_threads(self):
        '''Automatically create threads for each component and resolve connections'''
        for comp_name, comp in self._components.items():
            conn_list = []
            for conn_tuple in self._connections.items():
                conn = conn_tuple[1]
                if conn.get_dest_comp_name() == comp_name or conn.get_src_comp_name() == comp_name:
                    conn_list.append(conn)
            gen = False
            if isinstance(comp, (net_reader.NetReaderComponent, file_reader.FileReaderComponent)):
                gen = True
            #print(conn_list)
            thread = CompThread(comp, conn_list, gen)
            self.add_thread(thread)
        
    def run(self):
        '''Executes each of the registered CompThreads in the Application.'''
        for t in self._threads:
            t.start() 
                
    def join(self, quit_imm=False):
        '''Wait for all CompThreads to complete.'''
        got_quit = quit_imm
        i_string = ''
        if got_quit:
            for t in self._threads:
                t.time_to_stop = True
        while(len(self._threads) > 0):
            if not got_quit:
                try:
                    i_string = input()
                except EOFError:
                    pass
                #print(i_string)
                if i_string == 'quit':
                    got_quit = True
                    for t in self._threads:
                        t.time_to_stop = True
            for t in self._threads:
                t.join(1) 
            self._threads = [t for t in self._threads if t.is_alive()]
            '''
            except KeyboardInterrupt:
                for t in self._threads:
                    t.time_to_stop = True
            '''                    
    def parse_opts(self):
        usage = 'usage: %prog [opts] NODE_NAME'
        parser = optparse.OptionParser(usage)
        opts_tuple = parser.parse_args()
        args = opts_tuple[1]
        if(len(args) != 1):
            parser.error('No NODE_NAME specified')
        else:
            self.handle_name(args[0])

    def handle_name(self, name):
        self._node_name = name
    
    def usage(self, script_name):
        print('USAGE:\tpython3 ' + script_name + ' -n NODE_NAME\tNormal Usage')
        print('\tpython3 ' + script_name + ' -h\tPrint This Message')
    
    def execute(self, quit_imm=False, wait_time=0):
        '''Runs and then waits for the threads to finish.'''
        if not self._threads:
            self.create_threads()
        self.run()
        if wait_time > 0:
            time.sleep(wait_time)
        self.join(quit_imm)
        self.cleanup()
        
    def cleanup(self):
        '''Cleans up any leftover IO comps.'''
        for io_comp in self._io_comp_list:
            io_comp.cleanup()
        for conn in self._connections.values():
            if isinstance(conn, connector.InstrumentedConnector):
                conn.checkpoint()
            
    def resolve_abstractions(self):
        '''Resolve abstraction boundaries and create final list of connections and components'''
        
        abs_list = [(name, comp) for name, comp in self._components.items() if isinstance(comp, abstraction.Abstraction)]
        has_abs = lambda x: isinstance(x.get_src_comp(), abstraction.Abstraction) or isinstance(x.get_dst_comp(), abstraction.Abstraction)
        abs_conn_list = [conn_tuple[1] for conn_tuple in self._connections.items() if has_abs(conn_tuple[1])]

        for ab_tuple in abs_list:
            '''First recursively pull components and "clean" connections out of abstractions'''
            abs = ab_tuple[1]
            comp_list, conn_list = abs.resolve() 
            self.add_components(comp_list)
            self.add_connections(conn_list)
            #for comp in comp_list:
                #print(comp.get_name())
            '''Next, resolve endpoints of abstraction to implementations'''
            for port_tuple in abs._ports.items():
                #print(port_tuple)
                port = port_tuple[1]
                endp = connector.Endpoint(abs, port)
                #print(endp.get_name())
                for conn in abs_conn_list:
                    #print(conn.get_name())
                    new_conn = None
                    if endp.get_name() == conn._source.get_name():
                        new_conn = connector.Connector(abs.resolve_endpoint(port.get_name())._source, conn._dest)
                    if endp.get_name() == conn._dest.get_name():
                        new_conn = connector.Connector(conn._source, abs.resolve_endpoint(port.get_name())._dest)
                    if new_conn is not None:
                        '''Order here is important, don't want to delete the new_conn'''
                        self._connections.pop(conn.get_src_name())
                        self.add_connection(new_conn)
            self._components.pop(ab_tuple[0])
                
                
                
        
class CompThread(threading.Thread):
    '''
    Execution context for a Implementation. Inherited from threading.Thread. 
    '''
    
    def __init__(self, comp, conns, gen=False):
        '''
        Constructor
        
        Takes a Implementation, a list of Connectors connected to the Implementation, and an 
        optional argument to generate a fake input Connector (used by io_component.IOComponent instances).
        '''
        threading.Thread.__init__(self)
        if gen == True:
            conns.append(comp.get_dummy_conn())
        self._comp = comp
        self.__conns__ = conns
        self.time_to_stop = False
        
    def run(self):
        '''
        Run the selected component on its own thread
            1) go through all source connectors and read available msgs
            2) process message
            3) send all enqueued msgs from execution to their respective next hops
        '''
        try:
            #while(True and not self.time_to_stop):
            while(True):
                for conn in self.__conns__:
                    if(conn.get_dest_comp_name() == self._comp.get_name()):
                        try:
                            #print(self._comp.get_name() + ' checking conn ' + conn.get_name())
                            msg = conn.get()
                            #print(self._comp.get_name() + ' rcvd ' + msg)
                            run_again = self._comp.run_component(conn.get_dest_port_name(),msg)
                            if(run_again):
                                conn.put('RESTART')
                            for msgTuple in self._comp.get_enqueued_msgs():
                                if(msgTuple[0] == '__KILL__' and msgTuple[1] == '__KILL__'):
                                    raise ComponentFinished
                                for connDst in self.__conns__:
                                    if connDst.get_src_comp_name() == self._comp.get_name() and connDst.get_src_port_name() == msgTuple[0]:
                                        connDst.put(msgTuple[1])
                            self._comp.clear_enqueued_msgs()
                        except queue.Empty:
                            if self.time_to_stop:
                                raise ComponentFinished
                            #print('empty')
                            #pass
        except ComponentFinished:
            #print('Thread running ' + str(self._comp) + ' is ending')
            pass
        
    
class ComponentFinished(Exception):
    pass

